﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.IO;
using System.Diagnostics;
using System.Windows.Forms;
using System.ComponentModel;
using System.Xml;
using System.Xml.Linq;

namespace PSE.Tools.ProjectDependencyDiscovery
{
    public class DiscoveryManager
    {
        private List<string> _projetosOrdenados = new List<string>();
        private List<string> _referenciasQuebradas = new List<string>();
        private List<string> _skipList = new List<string>();
        private List<Projeto> _hierarquia = new List<Projeto>();
        private string _referenciaCiclica = string.Empty;

        private string _rootPath;

        public DiscoveryManager()
        {
            _skipList.Add(".sln");
            _skipList.Add(".vssscc");
            _skipList.Add(".vspscc");
            _skipList.Add(".dll");
        }

        private string GetFileNameFromProject(string projectFullPath)
        {
            XDocument document = XDocument.Load(projectFullPath);

            var assemblyName = (from c in document.Descendants()
                                where c.Name.LocalName.Equals("AssemblyName")
                                select (XElement)c).Single();

            var outputType = (from c in document.Descendants()
                              where c.Name.LocalName.Equals("OutputType")
                              select (XElement)c).Single();

            string fileExtension = "";

            switch (outputType.Value.ToUpper())
            {
                case "WINEXE":
                case "EXE":
                    fileExtension = ".exe";
                    break;
                case "LIBRARY":
                    fileExtension = ".dll";
                    break;
                default:
                    fileExtension = ".dll";
                    break;
            }

            return assemblyName.Value + fileExtension;
        }

        public List<string> DiscoverCompileOrder(List<string> projects)
        {
            List<Projeto> projectList = new List<Projeto>();

            foreach (string projectFullPath in projects)
            {
                Projeto project = new Projeto();
                project.NomeCompleto = projectFullPath;
                project.Nome = Path.GetFileName(projectFullPath);
                project.NomeAssembly = GetFileNameFromProject(projectFullPath);

                projectList.Add(project);
            }

            DiscoverProjectDependencies(projectList);

            List<string> referenciaCiclica = new List<string>();
            if (CheckProjectBuildRecursivity(projectList, ref referenciaCiclica))
                return referenciaCiclica;
            else
                return DiscoverProjectBuildSequence(projectList);
        }

        /// <summary>
        /// Funcao principal, que chama as outras funções na ORDEM correta
        /// </summary>
        /// <param name="Path"></param>
        public List<string> DiscoverDependencies(string path)
        {
            this._rootPath = path;

            // Lista todos os projetos existentes no caminho especificado.
            List<Projeto> projetos = DiscoverProjects(_rootPath);
            // Incrementa a lista com as dependencias dos projetos.
            DiscoverProjectDependencies(projetos);

            //CircularProjectDependencyDiscovery();
            List<String> projetosOrdenados = DiscoverProjectBuildSequence(projetos);

            return projetosOrdenados;
        }

        public string GetAssemblyInfoPath(string projectFullName)
        {
            const string AssemblyInfoFileName = "AssemblyInfo.cs";

            XmlManager xmlManager = new XmlManager();

            List<string> classes = xmlManager.ListarClassesProjeto(projectFullName);
            //string searchClass = classPath + "\\" + className;
            string result = classes.Find(
                delegate(string classToFind)
                {
                    return Path.GetFileName(classToFind).Equals(AssemblyInfoFileName);
                }
            );

            if (result != null)
                return result;
            else
                return String.Empty;
        }

        private string GetProject(string searchPath, string className, string rootPath)
        {
            if (String.IsNullOrEmpty(searchPath))
                throw new FileNotFoundException();

            XmlManager xmlManager = new XmlManager();

            DirectoryInfo dir = new DirectoryInfo(searchPath);

            FileInfo[] projetos = dir.GetFiles("*.csproj", SearchOption.TopDirectoryOnly);
            if (projetos.Length > 0)
            {
                foreach (FileInfo projeto in projetos)
                {
                    List<string> classes = xmlManager.ListarClassesProjeto(projeto.FullName);
                    //string searchClass = classPath + "\\" + className;
                    if (classes.Contains(className))
                        return projeto.FullName;
                    else
                    {
                        List<string> includes = xmlManager.ListarIncludesProjeto(projeto.FullName);
                        if (includes.Contains(className))
                            return projeto.FullName;
                    }
                }
                // Classe não encontrada, procurar em um nível abaixo
                string[] path = searchPath.Split('\\');
                string newPath = String.Empty;

                for (int i = 0; i < path.Length - 1; i++)
                    newPath = Path.Combine(newPath, path[i]);

                if (newPath.Equals(rootPath))
                {
                    string message = "Project file from the source code " + className + " not found.";
                    throw new FileNotFoundException(message);
                }
                //string newClassPath = Path.Combine(classPath, path[path.Length - 1]);
                //return GetProject(newPath, newClassPath, className);
                return GetProject(newPath, className, rootPath);
            }
            else
            {
                // Classe não encontrada, procurar em um nível abaixo
                if (dir.Parent == null)
                    return null;

                return GetProject(dir.Parent.FullName.ToString(), className, rootPath);
            }
        }

        public List<string> RecoverProjectFromClasses(string searchPath, List<string> classNames)
        {
            List<string> projetos = new List<string>();

            try
            {
                foreach (string classFullPath in classNames)
                {
                    if (File.Exists(classFullPath))
                    {

                        if (Path.GetExtension(classFullPath).Equals(".csproj"))
                        {
                            if (!projetos.Contains(classFullPath))
                                projetos.Add(classFullPath);
                        }
                        else if (!_skipList.Contains(Path.GetExtension(classFullPath)))
                        {
                            // Change class Team Foundation URI to a physical path
                            string classFullName = classFullPath.Replace('/', '\\');
                            classFullName = classFullName.Replace("$", searchPath);
                            //string projeto = GetProject(Path.GetDirectoryName(classFullName), Path.GetFileName(classFullName));
                            string projeto = GetProject(Path.GetDirectoryName(classFullName), classFullName, searchPath);
                            if (String.IsNullOrEmpty(projeto))
                            {
                                StringBuilder message = new StringBuilder();
                                message.Append("Project file from the source code " + classFullPath + " not found.");
                                throw new FileNotFoundException(message.ToString());
                            }

                            if (!projetos.Contains(projeto))
                                projetos.Add(projeto);
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }

            return projetos;
        }

        /// <summary>
        /// Faz uma verificação de todos os projetos que possam ter referencias ciclicas
        /// </summary>
        private bool CheckProjectBuildRecursivity(List<Projeto> projetos, ref List<string> referenciaCiclica)
        {
            foreach (Projeto proj in projetos)
            {
                _hierarquia.Clear();
                _hierarquia.Add(proj);

                CheckRecursivity(proj, proj);

                if (_hierarquia.Count.Equals(0))
                {
                    referenciaCiclica.Add(_referenciaCiclica);
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// Recursivamente, este algoritmo checa a hierarquia de projetos. Armazena numa coleção a hierarquia descoberta,
        /// verificando se algum projeto tenha como dependende o mesmo projeto que em algum nível qualquer herdou como referência.
        /// </summary>
        private bool CheckRecursivity(Projeto pai, Projeto dependencia)
        {
            bool hasDependency = false;

            if (dependencia != null)
            {
                foreach (Projeto dp in dependencia.Dependencias)
                {
                    var query = (from cl in _hierarquia
                                 where cl.Dependencias.Contains(pai)
                                 select cl).ToList();

                    if (query.Count > 0)
                    {
                        _referenciaCiclica = string.Format("{0} possui referencia ciclica com {1}", query[0].NomeCompleto, dp.NomeCompleto);
                        _hierarquia.Clear();
                        hasDependency = true;
                    }

                    if (!hasDependency)
                    {
                        _hierarquia.Add(dp);

                        hasDependency = CheckRecursivity(pai, dp);
                    }

                }
            }

            return hasDependency;
        }

        /// <summary>
        /// Gera a sequencia de Build de Projetos. A sequencia eh escrita no arquivo ProjectBuildSequence.txt.
        /// O algoritmo utilizado para gerar a sequencia basea-se na busca de folhas na arvore de dependencias jah existente,
        /// em cada etapa do algoritmo eh possivel "compilar" as folhas e marca-las como Resolvidas, buscando por novas "folhas"
        /// </summary>
        private List<string> DiscoverProjectBuildSequence(List<Projeto> projetos)
        {
            List<string> projetosOrdenados = new List<string>();

            while (projetos.Count > 0)
            {
                foreach (Projeto proj in projetos)
                {
                    if (((proj.Dependencias.Count - proj.DependenciasResolvidas) == 0)) //&& !proj.Resolvido)
                    {
                        if (proj.Pai != null)
                        {
                            foreach (Projeto projPai in proj.Pai)
                                projPai.ResolverDependencia();
                        }

                        // Projeto resolvido. Adiciona na lista de resolvidos
                        projetosOrdenados.Add(proj.NomeCompleto);

                        // Remove o projeto da lista de não resolvidos.
                        projetos.Remove(proj);
                        break;
                    }
                }
            }

            return projetosOrdenados;
        }

        public List<string> GetProjectsFromPath(string path)
        {
            List<string> projetos = new List<string>();
            DirectoryInfo dir = new DirectoryInfo(path);
            FileInfo[] files = dir.GetFiles("*.csproj", SearchOption.AllDirectories);

            foreach (FileInfo file in files)
                projetos.Add(file.FullName);

            return projetos;
        }

        /// <summary>
        /// Procura todos os arquivos com extensão .csproj no diretório definido pelo parâmetro "path".
        /// Os projetos encontrados são adicionados a lista global de projetos.
        /// </summary>
        /// <param name="path"></param>
        private List<Projeto> DiscoverProjects(string path)
        {
            List<Projeto> projetos = new List<Projeto>();

            DirectoryInfo dir = new DirectoryInfo(path);

            FileInfo[] files = dir.GetFiles("*.csproj", SearchOption.AllDirectories);

            if (files.Length > 0)
            {
                foreach (FileInfo file in files)
                {
                    Projeto proj = new Projeto();
                    proj.Nome = file.Name;
                    proj.NomeCompleto = file.FullName;
                    projetos.Add(proj);
                }
            }

            return projetos;
        }

        /// <summary>
        ///  Abre cada Projeto já instaciado e busca por dependencias de Classes e Projetos. 
        ///  Estas dependencias sao encontradas atraves de tags XML do arquivo do projeto.
        /// </summary>
        private void DiscoverProjectDependencies(List<Projeto> projetos)
        {
            // Verifica as dependencias de cada projeto e adiciona estas dependencias 
            // na coleção de projetos.
            foreach (Projeto proj in projetos)
            {
                XmlManager prf = new XmlManager();
                List<string> listaDependencia = prf.ListarDependenciaProjeto(proj.NomeCompleto);

                foreach (string s in listaDependencia)
                {
                    bool existe = false;
                    foreach (Projeto dep in projetos)
                    {
                        if (Path.GetExtension(s) == ".dll")
                        {
                            if (dep.NomeAssembly == s)
                            {
                                proj.Dependencias.Add(dep);
                                dep.Pai.Add(proj);
                                existe = true;
                            }
                        }
                        else if (dep.Nome == s)
                        {
                            proj.Dependencias.Add(dep);
                            dep.Pai.Add(proj);
                            existe = true;
                        }
                    }
                    if (!existe) // && isReferenciaQuebrada)
                        _referenciasQuebradas.Add(proj.Nome);
                }
            }
        }
    }
}
